/**
 * FocusManager.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This class manages the focus/blur state of the editor. This class is needed since some
 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
 *
 * This class will fire two events focus and blur on the editor instances that got affected.
 * It will also handle the restore of selection when the focus is lost and returned.
 *
 * @class tinymce.FocusManager
 */
define(
  'tinymce.core.FocusManager',
  [
    "tinymce.core.dom.DOMUtils",
    "tinymce.core.util.Delay",
    "tinymce.core.Env"
  ],
  function (DOMUtils, Delay, Env) {
    var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;

    var isUIElement = function (editor, elm) {
      var customSelector = editor ? editor.settings.custom_ui_selector : '';
      var parent = DOM.getParent(elm, function (elm) {
        return (
          FocusManager.isEditorUIElement(elm) ||
          (customSelector ? editor.dom.is(elm, customSelector) : false)
        );
      });
      return parent !== null;
    };

    var isInlineEditor = function (editor) {
      return editor.inline === true;
    };

    var isElementOursideInlineEditor = function (editor, target) {
      return isInlineEditor(editor) === false || editor.dom.isChildOf(target, editor.getBody()) === false;
    };

    /**
     * Constructs a new focus manager instance.
     *
     * @constructor FocusManager
     * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
     */
    function FocusManager(editorManager) {
      function getActiveElement() {
        try {
          return document.activeElement;
        } catch (ex) {
          // IE sometimes fails to get the activeElement when resizing table
          // TODO: Investigate this
          return document.body;
        }
      }

      // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
      // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
      function createBookmark(dom, rng) {
        if (rng && rng.startContainer) {
          // Verify that the range is within the root of the editor
          if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
            return;
          }

          return {
            startContainer: rng.startContainer,
            startOffset: rng.startOffset,
            endContainer: rng.endContainer,
            endOffset: rng.endOffset
          };
        }

        return rng;
      }

      function bookmarkToRng(editor, bookmark) {
        var rng;

        if (bookmark.startContainer) {
          rng = editor.getDoc().createRange();
          rng.setStart(bookmark.startContainer, bookmark.startOffset);
          rng.setEnd(bookmark.endContainer, bookmark.endOffset);
        } else {
          rng = bookmark;
        }

        return rng;
      }

      function registerEvents(e) {
        var editor = e.editor;

        editor.on('init', function () {
          // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
          if (editor.inline || Env.ie) {
            // Use the onbeforedeactivate event when available since it works better see #7023
            if ("onbeforedeactivate" in document && Env.ie < 9) {
              editor.dom.bind(editor.getBody(), 'beforedeactivate', function (e) {
                if (e.target != editor.getBody()) {
                  return;
                }

                try {
                  editor.lastRng = editor.selection.getRng();
                } catch (ex) {
                  // IE throws "Unexcpected call to method or property access" some times so lets ignore it
                }
              });
            } else {
              // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
              editor.on('nodechange mouseup keyup', function (e) {
                var node = getActiveElement();

                // Only act on manual nodechanges
                if (e.type == 'nodechange' && e.selectionChange) {
                  return;
                }

                // IE 11 reports active element as iframe not body of iframe
                if (node && node.id == editor.id + '_ifr') {
                  node = editor.getBody();
                }

                if (editor.dom.isChildOf(node, editor.getBody())) {
                  editor.lastRng = editor.selection.getRng();
                }
              });
            }
          }
        });

        editor.on('setcontent', function () {
          editor.lastRng = null;
        });

        // Remove last selection bookmark on mousedown see #6305
        editor.on('mousedown', function () {
          editor.selection.lastFocusBookmark = null;
        });

        editor.on('focusin', function () {
          var focusedEditor = editorManager.focusedEditor, lastRng;

          if (editor.selection.lastFocusBookmark) {
            lastRng = bookmarkToRng(editor, editor.selection.lastFocusBookmark);
            editor.selection.lastFocusBookmark = null;
            editor.selection.setRng(lastRng);
          }

          if (focusedEditor != editor) {
            if (focusedEditor) {
              focusedEditor.fire('blur', { focusedEditor: editor });
            }

            editorManager.setActive(editor);
            editorManager.focusedEditor = editor;
            editor.fire('focus', { blurredEditor: focusedEditor });
            editor.focus(true);
          }

          editor.lastRng = null;
        });

        editor.on('focusout', function () {
          Delay.setEditorTimeout(editor, function () {
            var focusedEditor = editorManager.focusedEditor;

            // Still the same editor the blur was outside any editor UI
            if (!isUIElement(editor, getActiveElement()) && focusedEditor == editor) {
              editor.fire('blur', { focusedEditor: null });
              editorManager.focusedEditor = null;

              // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
              if (editor.selection) {
                editor.selection.lastFocusBookmark = null;
              }
            }
          });
        });

        // Check if focus is moved to an element outside the active editor by checking if the target node
        // isn't within the body of the activeEditor nor a UI element such as a dialog child control
        if (!documentFocusInHandler) {
          documentFocusInHandler = function (e) {
            var activeEditor = editorManager.activeEditor, target;

            target = e.target;

            if (activeEditor && target.ownerDocument === document) {
              // Check to make sure we have a valid selection don't update the bookmark if it's
              // a focusin to the body of the editor see #7025
              if (activeEditor.selection && target !== activeEditor.getBody() && isElementOursideInlineEditor(editor, target)) {
                activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
              }

              // Fire a blur event if the element isn't a UI element
              if (target !== document.body && !isUIElement(activeEditor, target) && editorManager.focusedEditor === activeEditor) {
                activeEditor.fire('blur', { focusedEditor: null });
                editorManager.focusedEditor = null;
              }
            }
          };

          DOM.bind(document, 'focusin', documentFocusInHandler);
        }

        // Handle edge case when user starts the selection inside the editor and releases
        // the mouse outside the editor producing a new selection. This weird workaround is needed since
        // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
        if (editor.inline && !documentMouseUpHandler) {
          documentMouseUpHandler = function (e) {
            var activeEditor = editorManager.activeEditor, dom = activeEditor.dom;

            if (activeEditor.inline && dom && !dom.isChildOf(e.target, activeEditor.getBody())) {
              var rng = activeEditor.selection.getRng();

              if (!rng.collapsed) {
                activeEditor.lastRng = rng;
              }
            }
          };

          DOM.bind(document, 'mouseup', documentMouseUpHandler);
        }
      }

      function unregisterDocumentEvents(e) {
        if (editorManager.focusedEditor == e.editor) {
          editorManager.focusedEditor = null;
        }

        if (!editorManager.activeEditor) {
          DOM.unbind(document, 'selectionchange', selectionChangeHandler);
          DOM.unbind(document, 'focusin', documentFocusInHandler);
          DOM.unbind(document, 'mouseup', documentMouseUpHandler);
          selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
        }
      }

      editorManager.on('AddEditor', registerEvents);
      editorManager.on('RemoveEditor', unregisterDocumentEvents);
    }

    /**
     * Returns true if the specified element is part of the UI for example an button or text input.
     *
     * @method isEditorUIElement
     * @param  {Element} elm Element to check if it's part of the UI or not.
     * @return {Boolean} True/false state if the element is part of the UI or not.
     */
    FocusManager.isEditorUIElement = function (elm) {
      // Needs to be converted to string since svg can have focus: #6776
      return elm.className.toString().indexOf('mce-') !== -1;
    };

    FocusManager._isUIElement = isUIElement;

    return FocusManager;
  }
);
